package com.hcl.orm.core.dialect;

import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.TypeUtil;
import com.hcl.orm.core.exception.OrmException;
import com.hcl.orm.core.executor.page.PageParam;
import com.hcl.orm.core.executor.page.PageResult;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.*;

/**
 * @author hucl
 * @projectName hcl-orm
 * @description: TODO
 * @date 2021/8/1113:32
 */
public class Signature {
        private final Method method;
        private final boolean returnsMany;
        private final boolean returnsVoid;
        private final Class<?> returnType;
        private final boolean returnsPage;
        private final SortedMap<Integer, String> params;
        private final Integer pageParamIndex;
        private final boolean inputsEmpty;
        private final boolean inputsEntity;
        private final boolean inputsList;
        private final boolean inputsBasicArray;
        private final Class<?> inputRealType;

        public Method getMethod() {
            return method;
        }

        public boolean isReturnsMany() {
            return returnsMany;
        }

        public boolean isReturnsVoid() {
            return returnsVoid;
        }

        public boolean isReturnsPage() {
            return returnsPage;
        }

        public Integer getPageParamIndex() {
            return pageParamIndex;
        }

        public boolean isInputsEmpty() {
            return inputsEmpty;
        }

        public boolean isInputsEntity() {
            return inputsEntity;
        }

        public boolean isInputsList() {
            return inputsList;
        }

        public boolean isInputsBasicArray() {
            return inputsBasicArray;
        }

        public Class<?> getInputRealType() {
            return inputRealType;
        }

        public PageParam extractPageParam(Object[] args) {
            return (pageParamIndex != null) ? (PageParam) args[pageParamIndex] : null;
        }

        public SortedMap<Integer, String> getParams() {
            return params;
        }

        public Class<?> getReturnType() {
            return returnType;
        }

        public Signature(Method method) {
            this.method = method;
            this.returnType = method.getReturnType();
            this.returnsVoid = void.class.equals(this.returnType);
            this.returnsMany = this.returnType.isArray() || Collection.class.isAssignableFrom(this.returnType);
            this.returnsPage = PageResult.class.equals(this.returnType);
            this.pageParamIndex = getUniqueParamIndex(method, PageParam.class);
            this.params = Collections.unmodifiableSortedMap(getParams(method));
            Class<?> inputType = params.size() == 1 ? TypeUtil.getParamClass(method, this.params.keySet()
                    .iterator().next()) : null;

            this.inputsEmpty = params.size() == 0;
            this.inputsList = params.size() == 1 && Collection.class.isAssignableFrom(inputType);
            this.inputsEntity = params.size() == 1 && !inputsList &&
                    !ClassUtil.isSimpleValueType(TypeUtil.getParamClass(method, this.params.keySet()
                            .iterator().next()));

            this.inputRealType = inputsEntity ? inputType : inputsList ?
                    TypeUtil.getClass(((ParameterizedType) TypeUtil.getParamType(method, this.params.keySet()
                            .iterator().next()))
                            .getActualTypeArguments()[0]) : null;
            this.inputsBasicArray = params.size() > 1;

        }

        public Object convertArgsToSqlCommandParam(Object[] args) {
            final int paramCount = params.size();
            if (args == null || paramCount == 0) {
                return null;
            } else if (paramCount == 1) {
                return args[params.keySet().iterator().next()];
            } else {
                final Map<String, Object> param = new HashMap<>();
                int i = 0;
                for (Map.Entry<Integer, String> entry : params.entrySet()) {
                    param.put(entry.getValue(), args[entry.getKey()]);
                    final String genericParamName = method.getParameters()[entry.getKey()].getName();
                    if (!param.containsKey(genericParamName)) {
                        param.put(genericParamName, args[entry.getKey()]);
                    }
                    i++;
                }
                return param;
            }
        }

        private SortedMap<Integer, String> getParams(Method method) {
            final SortedMap<Integer, String> params = new TreeMap<>();
            final Class<?>[] argTypes = method.getParameterTypes();
            for (int i = 0; i < argTypes.length; i++) {
                if (!PageParam.class.isAssignableFrom(argTypes[i])) {
                    String paramName = method.getParameters()[i].getName();
                    params.put(i, paramName);
                }
            }
            return params;
        }

        private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
            Integer index = null;
            final Class<?>[] argTypes = method.getParameterTypes();
            for (int i = 0; i < argTypes.length; i++) {
                if (paramType.isAssignableFrom(argTypes[i])) {
                    if (index == null) {
                        index = i;
                    } else {
                        throw new OrmException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
                    }
                }
            }
            return index;
        }
}
